#!/usr/bin/env python
# -*- coding: UTF-8 -*-

import sys
import os
import os.path
from lxml import etree
from .commdef import (PACKAGE_NAME_SEPERATOR, 
        CommandType, ResultType, 
        DbType, MatchType, Direction, DynamicTextType,
        DbTable, DbColumn,
        DbModule, DbParameter, DbStatement, DbDynamic)
from .log import log_error, log_warn, log_info, log_verbose, log_good

# Constants.

STATEMENTS_XSD = '''<?xml version="1.0" ?>
<xsd:schema
    xmlns:xsd="http://www.w3.org/2001/XMLSchema"
    elementFormDefault="qualified">

    <xsd:element name="root">
        <xsd:complexType >
            <xsd:choice minOccurs="0" maxOccurs="unbounded" >
                <xsd:element name="module" type="moduleType" />
                <xsd:element name="include" type="includeType" />
            </xsd:choice>
        </xsd:complexType>
    </xsd:element>

    <xsd:complexType name="moduleType" >
        <xsd:sequence minOccurs="0" maxOccurs="1" >
            <xsd:element name="table" type="tableType" minOccurs="0" maxOccurs="unbounded" />
            <xsd:choice minOccurs="0" maxOccurs="unbounded" >
                <xsd:element name="statement" type="statementType" />
                <xsd:element name="stmt" type="statementType" />
            </xsd:choice>
        </xsd:sequence>
        <xsd:attribute name="name" type="identityName" use="required" />
        <xsd:attribute name="package" type="identityName" use="optional" />
        <xsd:attribute name="description" type="xsd:normalizedString" use="optional" default="" />
    </xsd:complexType>

    <xsd:complexType name="includeType" >
        <xsd:attribute name="src" type="xsd:string" use="required" />
    </xsd:complexType>

    <xsd:complexType name="tableType" >
        <xsd:choice minOccurs="1" maxOccurs="unbounded" >
            <xsd:element name="column" type="columnType" />
        </xsd:choice>
        <xsd:attribute name="name" type="identityName" use="required" />
        <xsd:attribute name="description" type="xsd:normalizedString" use="optional" default="" />
    </xsd:complexType>

    <xsd:complexType name="columnType" >
        <xsd:attribute name="name" type="identityName" use="required" />
        <xsd:attribute name="size" type="columnSizeType" use="optional" default="0" />
        <xsd:attribute name="type" type="columnTypeType" use="optional" default="string" />
        <xsd:attribute name="unique" type="boolean" use="optional" default="false" />
        <xsd:attribute name="primaryKey" type="boolean" use="optional" default="false" />
        <xsd:attribute name="description" type="xsd:normalizedString" use="optional" default="" />
        <xsd:attribute name="defaultValue" type="xsd:normalizedString" use="optional" />        
    </xsd:complexType>

    <xsd:complexType name="statementType" mixed="true" >
        <xsd:sequence>
            <xsd:element name="dynamic" type="dynamicStatementType" minOccurs="0" maxOccurs="unbounded" />
            <xsd:element name="parameter" type="parameterType" minOccurs="0" maxOccurs="unbounded" />
        </xsd:sequence>
        <xsd:attribute name="name" type="identityName" use="required" />
        <xsd:attribute name="allowPaging" type="boolean" default="auto" />
        <xsd:attribute name="commandType" type="statementCommandTypeType" default="text" />
        <xsd:attribute name="resultType" type="statementResultTypeType" default="updated-count" />
        <xsd:attribute name="resultTable" type="identityName" use="optional" />
        <xsd:attribute name="isReadOnly" type="boolean" default="auto" />
        <xsd:attribute name="description" type="xsd:normalizedString" use="optional" default="" />
    </xsd:complexType>

    <xsd:simpleType name="statementCommandTypeType" >
        <xsd:restriction base="xsd:normalizedString" >
            <xsd:enumeration value="text" />
            <!-- store procedure //-->
            <xsd:enumeration value="sp" />
        </xsd:restriction>
    </xsd:simpleType>

    <xsd:complexType name="dynamicStatementType" >
        <xsd:choice minOccurs="1" maxOccurs="unbounded">
            <xsd:element name="exists" type="dynamicStatementTextType" />
            <xsd:element name="notExists" type="dynamicStatementTextType" />
            <xsd:element name="isNull" type="dynamicStatementTextType" />
            <xsd:element name="isNotNull" type="dynamicStatementTextType" />
            <xsd:element name="isEmpty" type="dynamicStatementTextType" />
            <xsd:element name="isNotEmpty" type="dynamicStatementTextType" />
            <xsd:element name="isNullOrEmpty" type="dynamicStatementTextType" />
        </xsd:choice>
        <xsd:attribute name="prepend" type="xsd:normalizedString" use="optional" />
    </xsd:complexType>

    <xsd:complexType name="dynamicStatementTextType" mixed="true" >
        <xsd:attribute name="parameter" type="identityName" use="required" />
    </xsd:complexType>

    <xsd:complexType name="parameterType" >
        <xsd:attribute name="name" type="identityName" use="required" />
        <xsd:attribute name="type" type="columnTypeType" use="required" />
        <xsd:attribute name="matchType" type="parameterMatchType" use="optional" default="exact" />
        <xsd:attribute name="direction" type="directionType" use="optional" default="input" />
        <xsd:attribute name="size" type="columnSizeType" use="optional" default="0" >
        </xsd:attribute>
        <xsd:attribute name="description" type="xsd:normalizedString" use="optional" default="" />
    </xsd:complexType>

    <xsd:simpleType name="columnSizeType" >
        <xsd:restriction base="xsd:nonNegativeInteger" >
            <xsd:minInclusive fixed="true" value="0" />
            <xsd:maxInclusive fixed="true" value="4000" />
            <xsd:whiteSpace value="collapse" />
        </xsd:restriction>
    </xsd:simpleType>

    <xsd:simpleType name="parameterMatchType" >
        <xsd:restriction base="xsd:normalizedString" >
            <xsd:enumeration value="exact" />
            <xsd:enumeration value="starts-with" />
            <xsd:enumeration value="ends-with" />
            <xsd:enumeration value="contains" />
        </xsd:restriction>
    </xsd:simpleType>

    <xsd:simpleType name="directionType" >
        <xsd:restriction base="xsd:normalizedString">
            <xsd:enumeration value="input" />
            <xsd:enumeration value="output" />
            <xsd:enumeration value="inputOutput" />
        </xsd:restriction>
    </xsd:simpleType>

    <xsd:simpleType name="columnTypeType" >
        <xsd:restriction base="xsd:normalizedString" >
            <xsd:enumeration value="string" />
            <xsd:enumeration value="int" />
            <xsd:enumeration value="integer" />
            <xsd:enumeration value="long" />
            <xsd:enumeration value="money" />
            <xsd:enumeration value="decimal" />
            <xsd:enumeration value="currency" />
            <xsd:enumeration value="datetime" />
            <xsd:enumeration value="date" />
            <xsd:enumeration value="endDate" />
            <xsd:enumeration value="endMonth" />
            <xsd:enumeration value="endYear" />
            <xsd:enumeration value="float" />
            <xsd:enumeration value="double" />
            <xsd:enumeration value="bit" />
            <xsd:enumeration value="bool" />
            <xsd:enumeration value="boolean" />
        </xsd:restriction>
    </xsd:simpleType>

    <xsd:simpleType name="statementResultTypeType" >
        <xsd:restriction base="xsd:normalizedString" >
            <!-- Table -->
            <xsd:enumeration value="table" />
            <!-- Scalar -->
            <xsd:enumeration value="string" />
            <xsd:enumeration value="int" />
            <xsd:enumeration value="integer" />
            <xsd:enumeration value="short" />
            <xsd:enumeration value="long" />
            <xsd:enumeration value="money" />
            <xsd:enumeration value="decimal" />
            <xsd:enumeration value="currency" />
            <xsd:enumeration value="datetime" />
            <xsd:enumeration value="date" />
            <xsd:enumeration value="float" />
            <xsd:enumeration value="double" />
            <xsd:enumeration value="bit" />
            <xsd:enumeration value="bool" />
            <xsd:enumeration value="boolean" />
            <!-- List -->
            <xsd:enumeration value="list-string" />
            <xsd:enumeration value="list-int" />
            <xsd:enumeration value="list-integer" />
            <xsd:enumeration value="list-short" />
            <xsd:enumeration value="list-long" />
            <xsd:enumeration value="list-money" />
            <xsd:enumeration value="list-decimal" />
            <xsd:enumeration value="list-currency" />
            <xsd:enumeration value="list-datetime" />
            <xsd:enumeration value="list-date" />
            <xsd:enumeration value="list-float" />
            <xsd:enumeration value="list-double" />
            <xsd:enumeration value="list-bit" />
            <xsd:enumeration value="list-bool" />
            <xsd:enumeration value="list-boolean" />
            <!-- Update -->
            <xsd:enumeration value="uc" />
            <xsd:enumeration value="updated-count" />
        </xsd:restriction>
    </xsd:simpleType>

    <xsd:simpleType name="boolean" >
        <xsd:restriction base="xsd:string" >
            <xsd:enumeration value="auto" />
            <xsd:enumeration value="0" />
            <xsd:enumeration value="1" />
            <xsd:enumeration value="yes" />
            <xsd:enumeration value="no" />
            <xsd:enumeration value="true" />
            <xsd:enumeration value="false" />
        </xsd:restriction>
    </xsd:simpleType>

    <xsd:simpleType name="text" >
        <xsd:restriction base="xsd:string" >
            <xsd:minLength value="1" />
        </xsd:restriction>
    </xsd:simpleType>

    <xsd:simpleType name="identityName" >
        <xsd:restriction base="xsd:normalizedString" >
            <xsd:minLength value="1" />
            <xsd:pattern value="[\w_]+" />
        </xsd:restriction>
    </xsd:simpleType>
</xsd:schema>'''

# Configuration files.
__config_files = []

# Load lxml.
log_verbose('Loaded lxml library [%d.%d.%d.%d]' % etree.LXML_VERSION)

def get_bool(s, auto_value = False):
    '''
    '''
    if not s:
        return auto_value

    s = s.strip().lower()
    if s == 'auto':
        return auto_value
    else:
        return s == '1' or s == 'yes' or s == 'true'

def get_int(s):
    '''
    '''
    if not s:
        return 0

    s = s.strip().lower()
    return int(s)

def get_db_type(s):
    '''
    '''
    if not s:
        return DbType.STRING

    s = s.strip().lower()
    if s == 'string':
        return DbType.STRING
    elif s == 'integer' or s == 'int':
        return DbType.INTEGER
    elif s == 'long':
        return DbType.LONG
    elif s == 'money' or s == 'currency' or s == 'decimal':
        return DbType.DECIMAL
    elif s == 'datetime':
        return DbType.DATETIME
    elif s == 'date':
        return DbType.DATE
    elif s == 'enddate':
        return DbType.END_DATE
    elif s == 'endmonth':
        return DbType.END_MONTH
    elif s == 'endyear':
        return DbType.END_YEAR
    elif s == 'float':
        return DbType.FLOAT
    elif s == 'double':
        return DbType.DOUBLE
    elif s == 'bool' or s == 'bit' or s == 'boolean':
        return DbType.BOOL
    elif s == 'binary':
        return DbType.BINARY
    else:
        return DbType.STRING

def get_command_type(s):
    '''
    '''
    if not s:
        return CommandType.TEXT

    s = s.strip()
    if s == 'text':
        return CommandType.TEXT
    elif s == 'storeprocedure' or s == 'sp':
        return CommandType.STORE_PROCEDURE
    else:
        assert False, 'Unknown command type: "%s".' % s

def get_result_type(s):
    '''
    '''
    if not s:
        return ResultType.UPDATED_ROWS

    s = s.strip()
    if s ==  "table":
        return ResultType.TABLE
    elif s ==  "string":
        return ResultType.STRING
    elif s ==  "int" or s ==  "integer":
        return ResultType.INT32
    elif s ==  "short":
        return ResultType.INT16
    elif s ==  "long":
        return ResultType.INT64
    elif s ==  "float":
        return ResultType.FLOAT
    elif s ==  "double":
        return ResultType.DOUBLE
    elif s ==  "datetime" or s ==  "date":
        return ResultType.DATETIME;
    elif s ==  "money" or s ==  "currency" or s ==  "decimal":
        return ResultType.DECIMAL
    elif s ==  "bit" or s ==  "bool" or s ==  "boolean":
        return ResultType.BOOLEAN
    elif s ==  "list-string":
        return ResultType.STRING_LIST
    elif s ==  "list-int" or s ==  "list-integer":
        return ResultType.INT32_LIST
    elif s ==  "list-short":
        return ResultType.INT16_LIST
    elif s ==  "list-long":
        return ResultType.INT64_LIST
    elif s ==  "list-float":
        return ResultType.FLOAT_LIST
    elif s ==  "list-double":
        return ResultType.DOUBLE_LIST
    elif s ==  "list-datetime" or s ==  "list-date":
        return ResultType.DATETIME_LIST
    elif s ==  "list-money" or s ==  "list-currency" or s ==  "list-decimal":
        return ResultType.DECIMAL
    elif s ==  "list-bit" or s ==  "list-bool" or s ==  "list-boolean":
        return ResultType.BOOLEAN
    elif s ==  "updated-count":
        return ResultType.UPDATED_ROWS;
    else:
        assert False, 'Unknown result type: "%s".' % s

def get_match_type(s):
    '''
    '''
    if not s:
        return MatchType.EXACT

    s = s.strip()
    if s == 'exact':
        return MatchType.EXACT
    elif s == 'starts-with':
        return MatchType.STARTS_WITH
    elif s == 'ends-with':
        return MatchType.ENDS_WITH
    elif s == 'contains':
        return MatchType.CONTAINS
    else:
        assert False, 'Unknwon match type: "%s".' % s

def get_direction(s):
    '''
    '''
    if not s:
        return Direction.INPUT

    s = s.strip()
    if s == 'input':
        return Direction.INPUT
    elif s == 'inputOutput':
        return Direction.INPUT_OUTPUT
    elif s == 'output':
        return Direction.OUTPUT
    else:
        assert False, 'Unknown direction: "%s".' % s

def get_dynamic_text_type(s):
    '''
    '''
    assert s, 'Argument "s" cannot be null or empty!'

    s = s.strip()
    if s == 'exists':
        return DynamicTextType.EXISTS
    elif s == 'notExists':
        return DynamicTextType.NOT_EXISTS
    elif s == 'isNull':
        return DynamicTextType.IS_NULL
    elif s == 'isNotNull':
        return DynamicTextType.IS_NOT_NULL
    elif s == 'isEmpty':
        return DynamicTextType.IS_EMPTY
    elif s == 'isNotEmpty':
        return DynamicTextType.IS_NOT_EMPTY
    elif s == 'isNullOrEmpty':
        return DynamicTextType.IS_NULL_OR_EMPTY
    else:
        assert False, 'Unknown dynamic text: "%s".' % s

def strip_text(t):
    '''Strip text.
    '''
    if not t:
        return None
    else:
        return t.strip()

def make_full_package(parent_package, package):
    '''Combine parent package and package.
    '''
    if parent_package:
        if package:
            return parent_package + PACKAGE_NAME_SEPERATOR + name
        else:
            return parent_package
    else:
        if package:
            return package
        else:
            assert False, 'Argument "parent_package" and "package" cannot be null both!'

def init_parser():
    '''Initialize global variables for parser.
    '''
    global __config_files
    __config_files = []

def parse_config_file(config_file_name, root_package = None):
    '''Parse configuration file.
    '''
    assert config_file_name, 'Argument "config_file_name" cannot be null or empty!'

    if __config_files:
        last_config_file_name = __config_files[-1]
        last_config_file_base = os.path.dirname(last_config_file_name)
        config_file_name = os.path.join(last_config_file_base, config_file_name)

        if config_file_name in __config_files:
            raise RuntimeError('Duplicated config file: "%s" ' % config_file_name)
    else:
        config_file_name = os.path.abspath(config_file_name)

    log_verbose('Parsing file "%s" ...' % config_file_name)

    statements_schema = etree.XMLSchema(etree.fromstring(STATEMENTS_XSD))

    doc = etree.parse(config_file_name)

    assert doc, 'Variable "doc" should not be null!'

    if not statements_schema.validate(doc):
        validation_error = statements_schema.error_log.last_error
        raise RuntimeError('%s at %s:%d' % (validation_error.message, validation_error.filename, validation_error.line))

    # Push into
    __config_files.append(config_file_name)

    for child in doc.getroot().findall('*'):
        if child.tag == 'module':
            yield __process_config_module(child, root_package)
        elif child.tag == 'include':
            for m in __process_config_include(child, root_package):
                yield m
        else:
            assert False, 'Unknown element <%s /> at %s:%d' % (child.tag, __config_files[-1], child.sourceline)

    # Pop up
    del __config_files[-1]

    log_verbose('Parsed file "%s".' % config_file_name)

def __process_config_module(module, root_package):
    '''
    '''
    assert module is not None, 'Argument "module" cannot be null!'

    name = module.get('name')
    package = module.get('package')
    description = module.get('description', '')
    statements = []
    tables = []

    log_verbose('  Parsing module "%s" (%s:%d) ...' % (name, __config_files[-1], module.sourceline))

    children = module.xpath('table')
    for child in children:
        table = __process_config_table(child)
        tables.append(table)

    children = module.xpath('statement|stmt')
    for child in children:
        stmt = __process_config_statement(child)
        statements.append(stmt)

    nmodule = DbModule()
    nmodule.package = make_full_package(root_package, package)
    nmodule.name = name
    nmodule.description = description
    nmodule.tables = tables
    nmodule.statements = statements
    nmodule.line_info = '%s:%d' % (__config_files[-1], module.sourceline)

    log_verbose('  Parsed module "%s" (%s:%d).' % (name, __config_files[-1], module.sourceline))

    return nmodule

def __process_config_table(table):
    '''
    '''
    name = table.get('name')
    description = table.get('description', '')

    log_verbose('    Parsing table "%s" (%s:%d) ...' % (name,
        __config_files[-1], table.sourceline))

    columns = []

    for child in table:
        if child.tag == 'column':
            nc = __process_config_column(child)
            columns.append(nc)
        else:
            assert False, 'Unknown element <%s /> at %s:%d' % (child.tag, __config_files[-1], child.sourceline)

    ntable = DbTable()
    ntable.name = name
    ntable.description = description
    ntable.columns = columns
    ntable.line_info = '%s:%d' % (__config_files[-1], table.sourceline)

    log_verbose('    Parsed table "%s" (%s:%d).' % (name, __config_files[-1],
        table.sourceline))

    return ntable

def __process_config_column(column):
    '''
    '''
    nc = DbColumn()
    nc.name = column.get('name')
    nc.db_type = get_db_type(column.get('type', 'string'))
    nc.size = get_int(column.get('size', '0'))
    nc.description = column.get('description', '')
    nc.unique = get_bool(column.get('unique', 'false'))
    nc.primary_key = get_bool(column.get('primaryKey', 'false'))
    nc.default_value = column.get('defaultValue', None)
    nc.line_info = '%s:%d' % (__config_files[-1], column.sourceline)
    return nc

def __process_config_statement(statement):
    '''
    '''
    name = statement.get('name')
    command_type = statement.get('commandType', 'text')
    allow_paging = statement.get('allowPaging', 'auto')
    is_readonly = statement.get('isReadOnly', 'auto')
    result_type = statement.get('resultType', 'updated-count')
    result_table = statement.get('resultTable', None)
    description = statement.get('description', '')

    if result_table and result_type != 'table':
        log_warn('    Attribute "resultTable" has been specified but attribute "resultType" is not "table" (%s:%d).' % (__config_files[-1], statement.sourceline))

    log_verbose('    Parsing statement "%s" (%s:%d) ...' % (name, __config_files[-1], statement.sourceline))

    texts = []
    parameters = []
    default_parameter_flag = False

    text0 = strip_text(statement.text)
    if text0:
        texts.append(text0)

    for child in statement:
        if child.tag == 'dynamic':
            texts.append(__process_config_dynamic(child))
        elif child.tag == 'parameter':
            np = __process_config_parameter(child)
            parameters.append(np)

            if np.direction != Direction.INPUT_OUTPUT and np.direction != Direction.OUTPUT:
                default_parameter_flag = True
            elif default_parameter_flag:
                raise RuntimeError(
                        'Output or inputoutput parameter cannot be placed after any input parameter at %s:%d.' 
                        % (__config_files[-1], child.sourceline))
        else:
            assert False, 'Unknown element <%s /> at %s:%d' % (child.tag, __config_files[-1], child.sourceline)

        textn = strip_text(child.tail)
        if textn:
            texts.append(textn)

    nstmt = DbStatement()
    nstmt.name = name
    nstmt.command_type = get_command_type(command_type)
    nstmt.allow_paging = get_bool(allow_paging)
    nstmt.result_type = get_result_type(result_type)
    nstmt.result_table = result_table
    nstmt.is_readonly = get_bool(is_readonly, nstmt.result_type != ResultType.UPDATED_ROWS)
    nstmt.texts = texts
    nstmt.description = description
    nstmt.parameters = parameters
    nstmt.line_info = '%s:%d' % (__config_files[-1], statement.sourceline)

    log_verbose('    Parsed statement "%s" (%s:%d).' % (name, __config_files[-1], statement.sourceline))

    return nstmt

def __process_config_include(include, root_package):
    '''
    '''
    assert include is not None, 'Argument "include" cannot be null!'

    config_file_name = include.get('src')
    return parse_config_file(config_file_name, root_package)

def __process_config_dynamic(dynamic):
    '''
    '''
    assert dynamic is not None, 'Argument "dynamic" cannot be null!'

    prepend = dynamic.get('prepend')

    dy = DbDynamic()

    for child in dynamic.findall('*'):
        dt = __process_config_dynamic_text(child)
        if dt:
            dy.append(dt)

    if prepend:
        dy.prepend = prepend
    else:
        dy.prepend = ''

    dy.line_info = '%s:%d' % (__config_files[-1], dynamic.sourceline)

    return dy

def __process_config_dynamic_text(dt):
    '''
    '''
    assert dt is not None, 'Argument "dt" cannot be null!'

    pn = dt.get('parameter')
    assert pn, 'Attribute "parameter" cannot be null or empty at %s:%d' % (__config_files[-1], dt.sourceline)

    text = dt.text

    dt_type = get_dynamic_text_type(dt.tag)

    return (dt_type, pn, text)

def __process_config_parameter(parameter):
    '''
    '''
    np = DbParameter()
    np.name = parameter.get('name')
    np.db_type = get_db_type(parameter.get('type', 'string'))
    np.match_type = get_match_type(parameter.get('matchType', 'exact'))
    np.direction = get_direction(parameter.get('direction', 'input'))
    np.size = get_int(parameter.get('size', '0'))
    np.description = parameter.get('description', '')
    np.line_info = '%s:%d' % (__config_files[-1], parameter.sourceline)
    return np
